package ss.pku.jsa.type.inference;

import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.LabeledStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class TypeClosure {
	public CallGraph cg = new CallGraph();
	CHAGraph cha;
	private Set<ICompilationUnit> typeSet = new HashSet();
	private Set<MethodKey> methodSet = new HashSet();

	public Set<MethodKey> visitedMethodSet = new HashSet();

	public Set<ICompilationUnit> visitedTypeSet = new HashSet();
	private Set<IJavaProject> projects;
	private Set<ICompilationUnit> originalTypes;
	private Set<MethodKey> originalMethods = new HashSet();

	private Map<TypeKey, ICompilationUnit> typeMap = new HashMap();

	private Set<TypeKey> missingTypes = new HashSet();
	private Set<String> unresolvedTypes = new HashSet();

	public boolean needsVisit(MethodKey m) {
		if (this.visitedMethodSet.contains(m))
			return false;
		if (this.methodSet.contains(m)) {
			return true;
		}

		Set<MethodKey> overridden = this.cha.getOverriddenSet(m);
		if (overridden == null)
			return false;
		for (MethodKey o : overridden) {
			if (this.methodSet.contains(o)) {
				return true;
			}
		}

		return false;
	}

	public void add(ICompilationUnit c) {
		this.typeSet.add(c);
		addOverridingMethods(c);
	}

	public void add(MethodKey m) {
		if (this.methodSet.add(m)) {
			ICompilationUnit c = findType(m.getDeclaringType());

			if (c != null)
				add(c);
		}
	}

	public void markVisited(MethodKey m) {
		this.visitedMethodSet.add(m);
	}

	public TypeClosure(Set<IJavaProject> projects,
			Set<ICompilationUnit> originalTypes, CHAGraph cha) {
		this.projects = projects;
		this.originalTypes = originalTypes;
		this.cha = cha;
		for (ICompilationUnit t : originalTypes)
			add(t);
		run();
	}

	private void run() {
		ASTParser parser = ASTParser.newParser(3);
		while (!this.typeSet.isEmpty()) {
			ICompilationUnit t = (ICompilationUnit) this.typeSet.iterator()
					.next();
			this.typeSet.remove(t);
			this.visitedTypeSet.add(t);
			try {
				parser.setSource(t);
				parser.setResolveBindings(true);
				CompilationUnit compilationUnit = (CompilationUnit) parser
						.createAST(null);
				String fileName = t.getUnderlyingResource().getName();
				Visitor visitor = new Visitor(compilationUnit, fileName,
						this.originalTypes.contains(t));
				compilationUnit.accept(visitor);
			} catch (JavaModelException e) {
				throw new RuntimeException("JME: " + e);
			}
		}
		for (TypeKey t : this.missingTypes) {
			JqualPlugin.printlnLog("missingTypes", t.name);
		}

		for (MethodKey m : this.visitedMethodSet) {
			Set<MethodKey> overridden = this.cha.getOverriddenSet(m);

			if (overridden != null)
				for (MethodKey o : overridden) {
					if (!this.visitedMethodSet.contains(o))
						continue;
					this.cg.addEdge(o, m);
				}
		}
	}

	public ICompilationUnit findType(TypeKey t) {
		ICompilationUnit result = (ICompilationUnit) this.typeMap.get(t);
		if (result != null) {
			return result;
		}

		if (this.missingTypes.contains(t))
			return null;
		try {
			Iterator p = this.projects.iterator();
			do {
				IJavaProject j = (IJavaProject) p.next();
				IType found = j.findType(t.name);

				if (found != null) {
					result = found.getCompilationUnit();
					if (result != null) {
						this.typeMap.put(t, result);
						return result;
					}
				}
				if (result != null)
					break;
			} while (p.hasNext());
		} catch (JavaModelException e) {
			e.printStackTrace();
			throw new RuntimeException("JavaModelException in findType");
		}
		this.missingTypes.add(t);
		return null;
	}

	private void addOverridingMethods(ICompilationUnit c) {
		if (this.methodSet.isEmpty())
			return;
		try {
			IType[] types = c.getTypes();
			for (IType t : types) {
				IMethod[] methods = t.getMethods();
				for (IMethod s : methods) {
					Profile.logMemStart("addOverridingMethods/new MethodKey");
					MethodKey sm = MethodKey.getMethodKey(s);
					Profile.logMemEnd();
					if (this.methodSet.contains(sm))
						continue;
					Set<MethodKey> overridden = this.cha.getOverriddenSet(sm);
					if (overridden == null)
						continue;
					for (MethodKey o : overridden)
						if (this.methodSet.contains(o)) {
							add(sm);
							break;
						}
				}
			}
		} catch (JavaModelException e) {
			throw new RuntimeException("JME: " + e);
		}
	}

	public static boolean isStatic(MethodDeclaration m) {
		return (m.getModifiers() & 0x8) != 0;
	}

	public static class CallGraph implements Serializable {
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		Map<MethodKey, Set<MethodKey>> g = new HashMap();

		public boolean addEdge(MethodKey caller, MethodKey callee) {
			Set s = (Set) this.g.get(caller);
			if (s == null) {
				s = new HashSet();
				this.g.put(caller, s);
			}
			return s.add(callee);
		}

		public Set<MethodKey> reachableFromSet(Set<MethodKey> s) {
			Set ret = new HashSet(s);
			Set workset = new HashSet(s);
			while (!workset.isEmpty()) {
				MethodKey caller = (MethodKey) workset.iterator().next();
				workset.remove(caller);
				Set<MethodKey> callees = (Set) this.g.get(caller);
				if (callees != null) {
					for (MethodKey callee : callees) {
						if (!ret.contains(callee)) {
							workset.add(callee);
							ret.add(callee);
						}
					}
				}
			}
			return ret;
		}

		public String toString() {
			StringBuilder ret = new StringBuilder();
			for (MethodKey caller : this.g.keySet()) {
				ret.append(caller + " -> ");
				for (MethodKey callee : (Set<MethodKey>) this.g.get(caller))
					ret.append(callee + " ");
				ret.append("\n");
			}
			return ret.toString();
		}
	}

	public class Visitor extends ASTVisitor {
		protected CompilationUnit compilationUnit = null;
		protected String fileName = null;
		boolean original;
		private Stack<MethodKey> currentMethodStack = new Stack();

		boolean includeInits = false;

		public Visitor(CompilationUnit compilationUnit, String fileName,
				boolean original) {
			this.compilationUnit = compilationUnit;
			this.fileName = fileName;
			this.original = original;
		}

		public boolean visit(TypeDeclaration node) {
			if (this.includeInits) {
				Profile.logMemStart("visit(TypeDeclaration)/new MethodKey");
				MethodKey k = MethodKey.getInitMethodKey(node.resolveBinding());
				Profile.logMemEnd();
				this.currentMethodStack.push(k);
			}
			return true;
		}

		public void endVisit(TypeDeclaration node) {
			if (this.includeInits)
				this.currentMethodStack.pop();
		}

		public boolean visit(MethodDeclaration node) {
			IMethodBinding b = node.resolveBinding();
			Profile.logMemStart("visit(MethodDeclaration)/new MethodKey");
			MethodKey m = MethodKey.getMethodKey(b);
			Profile.logMemEnd();
			this.currentMethodStack.push(m);

			if (this.original) {
				TypeClosure.this.originalMethods.add(m);
			}
			if ((this.original) || (TypeClosure.this.needsVisit(m))) {
				TypeClosure.this.markVisited(m);

				return true;
			}
			return false;
		}

		public void endVisit(MethodDeclaration node) {
			this.currentMethodStack.pop();
		}

		public boolean visit(MethodInvocation node) {
			IMethodBinding b = node.resolveMethodBinding();
			Profile.logMemStart("visit(MethodInovcation)/new MethodKey");
			MethodKey m = MethodKey.getMethodKey(b);
			Profile.logMemEnd();
			TypeClosure.this.add(m);
			if (!this.currentMethodStack.isEmpty())
				TypeClosure.this.cg.addEdge(
						(MethodKey) this.currentMethodStack.peek(), m);
			return true;
		}

		public boolean visit(SuperConstructorInvocation node) {
			IMethodBinding b = node.resolveConstructorBinding();
			Profile.logMemStart("visit(SuperConstructorInvocation)/new MethodKey");
			MethodKey m = MethodKey.getMethodKey(b);
			Profile.logMemEnd();
			TypeClosure.this.add(m);
			if (!this.currentMethodStack.isEmpty())
				TypeClosure.this.cg.addEdge(
						(MethodKey) this.currentMethodStack.peek(), m);
			return true;
		}

		public boolean visit(SuperMethodInvocation node) {
			IMethodBinding b = node.resolveMethodBinding();
			Profile.logMemStart("visit(SuperMethodInvocation)/new MethodKey");
			MethodKey m = MethodKey.getMethodKey(b);
			Profile.logMemEnd();
			TypeClosure.this.add(m);
			if (!this.currentMethodStack.isEmpty())
				TypeClosure.this.cg.addEdge(
						(MethodKey) this.currentMethodStack.peek(), m);
			return true;
		}

		public boolean visit(BreakStatement node) {
			return false;
		}

		public boolean visit(ContinueStatement node) {
			return false;
		}

		public boolean visit(ImportDeclaration node) {
			return false;
		}

		public void endVisit(SimpleName node) {
			if (!this.includeInits) {
				return;
			}

			if ((node.getParent() instanceof LabeledStatement)) {
				return;
			}
			ITypeBinding t = node.resolveTypeBinding();
			getLocation(node);

			if (t == null) {
				TypeClosure.this.unresolvedTypes.add(node.toString());
				return;
			}

			if ((!t.isClass()) && (!t.isInterface())) {
				return;
			}

			Profile.logMemStart("endVisit(SimpleName)/new TypeKey");
			TypeKey k = TypeKey.getTypeKey(t);
			Profile.logMemEnd();
			ICompilationUnit cu = TypeClosure.this.findType(k);

			if ((cu != null) && (!TypeClosure.this.visitedTypeSet.contains(cu)))
				TypeClosure.this.add(cu);
		}

		public boolean visit(ClassInstanceCreation node) {
			IMethodBinding b = node.resolveConstructorBinding();
			Profile.logMemStart("visit(ClassInstanceCreation)/new MethodKey");
			MethodKey m = MethodKey.getMethodKey(b);
			Profile.logMemEnd();
			TypeClosure.this.add(m);
			if (!this.currentMethodStack.isEmpty())
				TypeClosure.this.cg.addEdge(
						(MethodKey) this.currentMethodStack.peek(), m);
			return true;
		}

		private Location getLocation(ASTNode node) {
			return Location.getLocation(node, this.compilationUnit,
					this.fileName);
		}
	}
}
